#include <iostream>
#include <fstream>
#include <stdio.h>
#include <cstdlib>
#include <pthread.h>
#include <cstdint>
#include <unistd.h>
#include <map>
#include <string.h>
#include <cstdint>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <list>
#include <signal.h>
#include "common_values.h"
#include "sock.h"
#include "sockdist.h"

#define FILES_DIRECTORY_C "./filesServer/"

using namespace std;

/*
 * le makefile appelle cette ligne de commande
 * Pour compiler : g++ -lpthread -std=c++11 -o test server.cc sock.cc sockdist.cc
 */

typedef struct parameters_struct
{
  int desc; //Descripteur du circuit virtuel
  pthread_mutex_t * lock;
  pthread_cond_t * cond;
  list<string> * toProcess;
  bool go_on;
} parameters_t;

typedef struct state_struct
{
  uint16_t readings = 0; //Nombre de lectures
  pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //Accès en écriture
  uint32_t size = 0;
} state_t; //Existe pour chaque fichier du dossier

bool verbose = true;

map<string,state_t> file_list;
pthread_mutex_t lock_file_list = PTHREAD_MUTEX_INITIALIZER; //Utilisé si delete ou rename 

int sig_brpub;
unsigned long nb_of_clients=0;
pthread_mutex_t lock_nb_of_clients = PTHREAD_MUTEX_INITIALIZER; //Utilisé à chaque connexion/déconnexion d'un client


void * threadEmission(void * param);
void * threadReception(void * param);
void * threadClient(void * param);

void sighandler(int signum)
{
  cout<<"\n# Fermeture manuelle du server, fermeture des boites réseaux..."<<endl;
  shutdown(sig_brpub,SHUT_RDWR);
  close(sig_brpub);
  exit(1);
}

int updateFileList()
{
  state_t init_state;
  DIR *dir;
  struct dirent *ent;
  pthread_mutex_lock(&lock_file_list);
  if ((dir = opendir ("./filesServer/")) != NULL)
  {
    while ((ent = readdir (dir)) != NULL) {
      if (ent->d_type == DT_REG) //Fichier normal (regular file), donc pas de sous-dossiers
	file_list.emplace(ent->d_name,init_state);
    }
    closedir (dir);
  }
  else
  {
    //Dossier impossible à lire
    pthread_mutex_unlock(&lock_file_list);
    perror ("");
    return EXIT_FAILURE;
  }
  cout <<"-> "<< file_list.size() <<" fichiers sur le serveur"<<endl;
  pthread_mutex_unlock(&lock_file_list);
  return 0;
}

int deleteFile(int desc)
{
  //cout << "Début de fonction de suppression"<<endl;
  uint16_t path_len;
  char * c_path;
  string path;
  // On reçoit la taille du path fichier
  if (-1==recv(desc,&path_len,sizeof(path_len),0))
  {
    perror("");
    return -1;
  }
  
  path_len = ntohs(path_len);
  c_path = new char [path_len];
  if (-1==recv(desc,c_path,path_len,0)) //On reçoit le nom de fichier
  {
    perror("");
    return -1;
  }
  path=c_path;
  delete [] c_path;
  int code;
  pthread_mutex_lock(&lock_file_list);
  map<string,state_t>::iterator it;
  if ((it=file_list.find(path)) == file_list.end()) //L'élément n'existe pas
  {
    code = htons(0);
    if(-1==send(desc,&code,sizeof(code),0))
      {
	pthread_mutex_unlock(&lock_file_list);
	cerr<< "Erreur lors de l'envoi du code"<<endl;
	return -1;
      }
    
    pthread_mutex_unlock(&lock_file_list);
    _V cout<<"Demande de suppression d'un fichier inexistant : "<< path <<endl;
    return 0;
    
  }

  if (pthread_mutex_trylock(&(file_list[path].lock)))
  {
    //On renvoie -1 pour dire que le fichier est actuellement modifié et qu'il ne peut être supprimé !
    pthread_mutex_unlock(&lock_file_list);
    code = htons(-1);
    if (-1==send(desc,&code,sizeof(code),0))
    {
      pthread_mutex_unlock(&lock_file_list);
      cerr<<"Erreur lors de l'envoi du code"<<endl;
      return -1;
    }
    cout << "Fichier en cours de modif " << endl;
    pthread_mutex_unlock(&lock_file_list);
    return 0; //N'est pas une erreur côté serveur
  }
  //Ici on possède le verrou sur le fichier, il ne peut donc pas être modifié
  string complete_path=FILES_DIRECTORY_C + path;
  if (unlink(complete_path.c_str()))
    {
      code = htons(-2);
      if (-1==send(desc,&code,sizeof(code),0))
	{
	  cerr<<"Erreur lors de l'envoi du code"<<endl;
	  pthread_mutex_unlock(&(file_list[path].lock));
	  pthread_mutex_unlock(&lock_file_list);
	  return -1;
	}
      cerr<<"Erreur lors de la suppression du fichier"<<endl;
      perror("");
      pthread_mutex_unlock(&(file_list[path].lock));
      pthread_mutex_unlock(&lock_file_list);
      return -1;
    }
  code = htons(1);
  if (-1==send(desc,&code,sizeof(code),0))
    {
      cerr<<"Erreur lors de l'envoi du code"<<endl;
      return -1;
    }
  file_list.erase(it);
  //pthread_mutex_unlock(&(file_list[path].lock));
  pthread_mutex_unlock(&lock_file_list);
  
  return 0;
}
  
int sendList(int desc)
{
  string f_list;
  for (map<string,state_t>::iterator it = file_list.begin(); it != file_list.end(); it++)
  {
    f_list.append(it->first); //Le nom de fichier
    f_list.append("//"); //Le séparateur
  }
  uint32_t f_size = f_list.size();
  uint32_t nb_files = file_list.size();
  if (send(desc,&nb_files,sizeof(nb_files),0)==-1)
  {
    cerr<<"Erreur lors de l'envoi du nombre de fichiers"<<endl;
    return EXIT_FAILURE;
  }
  if (send(desc,&f_size,sizeof(f_size),0)==-1)
  {
    cerr<<"Erreur lors de l'envoi de la taille de la liste"<<endl;
    return EXIT_FAILURE;
  }
  if (send(desc,f_list.c_str(),f_size,0)==-1)
  {
    cerr<<"Erreur lors de l'envoi de la liste"<<endl;
    return EXIT_FAILURE;
  }
  return 0;
}

int getFile(int descSec, list<string> *toDownload, pthread_mutex_t &lock_toDownload)
{
  uint32_t f_net_size=-1, f_size=0;
  uint16_t net_path_len=0, path_len; //Limité à 16 bits, par sécurité (65535)
  if (-1==recv(descSec,&net_path_len,sizeof(net_path_len),0))
  {
    perror("");
    return -1;
  }
  path_len = ntohs(net_path_len);
  if (path_len <= 0)
  {
    _V cerr<<"Fichier trop petit pour être accepté"<<endl;
    return 0;
  }
  char * c_path = new char[path_len+1];
  if (-1==recv(descSec,c_path,path_len,0)) //On reçoit le nom de fichier
  {
    perror("");
    return -1;
  }
  c_path[path_len]='\0';
  string path = c_path;

  if (-1==recv(descSec,&f_net_size,sizeof(f_net_size),0)) //Taille du fichier
  {
    perror("");
    return -1;
  }
  f_size=ntohl(f_net_size);

  if (f_size == (uint32_t)-1) //Cas d'erreur (fichier introuvable par exemple)
  {
    _V cerr<<"Abandon de l'upload par le client"<<endl;
    return 1;
  }
  
  pthread_mutex_lock(&lock_file_list);
  file_list.find(path);
  if ((file_list.find(path) != file_list.end())) //Le fichier existe déjà
  {
    pthread_mutex_unlock(&lock_file_list);
    f_size = htonl(-1);
    if (-1==send(descSec,&f_size,sizeof(f_size),0)) //On envoie -1 -> FICHIER EXISTANT
      return -1;
    return 0; //N'est pas considéré comme une erreur de la part du serveur
  }
  else
  {
    if (-1==send(descSec,&f_net_size,sizeof(f_net_size),0))
    {
      return -1;
    }
  }
  
  state_t init_state;
  file_list.emplace(path,init_state);
  pthread_mutex_lock(&(file_list[path].lock)); //Verouillage du fichier (en cours de modification)
  file_list[path].size=f_size;
  pthread_mutex_unlock(&lock_file_list);

  pthread_mutex_lock(&lock_toDownload);
  toDownload->push_back(path);
  pthread_mutex_unlock(&lock_toDownload);

  return 0;
}

int sendFile(int descSec, list<string> &toUpload, pthread_mutex_t &lock_toUpload)
{     //Récuperer le nom du fichier,vérifier qu'il existe, préciser qu'on le lit en incrémentant le nombre de lecture
      //et envoyer le fichier (après avoir renseigné la taille)
  uint16_t path_len=0; //Limité à 16 bits, par sécurité (65535)
  if (-1==recv(descSec,&path_len,sizeof(path_len),0))
  {
    perror("");
    return -1;
  }
  path_len = ntohs(path_len);
  if (path_len == 0)
  {
    _V cerr<<"Le client ne demande aucun fichier en particulier..."<<endl;
    return 0;
  }
  char * c_path = new char[path_len];
  if (-1==recv(descSec,c_path,path_len,0)) //On reçoit le nom de fichier
  {
    perror("");
    return -1;
  }
 
  string path = c_path;
  string complete_path = FILES_DIRECTORY_C;
  complete_path+=path;
  delete [] c_path;
  _V cout<<"Le client demande le téléchargement de "<<path<<" ("<<complete_path<<")"<<endl;
  
  int32_t f_size = htonl(-1);
  pthread_mutex_lock(&lock_file_list);
  if (file_list.find(path) == file_list.end()) //L'élément n'existe pas
  {
    pthread_mutex_unlock(&lock_file_list);
    if (-1==send(descSec,&f_size,sizeof(f_size),0)) //On envoie -1 -> FICHIER INEXISTANT
      return -1;
    return 0; //N'est pas considéré comme une erreur de la part du serveur
  }
  struct timespec t1;
  clock_gettime(CLOCK_REALTIME,&t1);
  t1.tv_sec+=1; //Pause d'une seconde maximum
  if (pthread_mutex_timedlock(&(file_list[path].lock),&t1)) //Déjà verrouillé par quelqu'un d'autre
  {
    pthread_mutex_unlock(&lock_file_list);
    if (-1==send(descSec,&f_size,sizeof(f_size),0)) //Là aussi on envoie -1 -> FICHIER EN COURS DE MODIFICATION
      return -1;
    return 0; //N'est pas considéré comme une erreur de la part du serveur
  }
  //pthread_mutex_lock(&(file_list[path].lock));
  file_list[path].readings+=1;
  pthread_mutex_unlock(&(file_list[path].lock));
  pthread_mutex_unlock(&lock_file_list);

  struct stat stat_buf; //Pour récupérer la taille du fichier
  int rc = stat(complete_path.c_str(), &stat_buf);
  if (rc) //Si erreur lors de la récupération des informations
  {
    f_size = -1;
    cerr<<"Erreur lors du stat du fichier"<<endl;
  }
  else
    f_size = stat_buf.st_size;
  f_size = htonl(f_size);
  if ((-1==send(descSec,&f_size,sizeof(f_size),0)) || (f_size==-1)) //On envoie la taille du fichier, si c'est négatif on quitte
    return -1; //Ici erreur côté serveur car soit l'envoi a échoué, soit le fichier est introuvable mais référencé dans la table

  cerr<<"Taille : "<<ntohl(f_size)<<endl;
  pthread_mutex_lock(&lock_toUpload);
  toUpload.push_back(path); //Ajout du fichier à la liste des fichiers à traiter
  pthread_mutex_unlock(&lock_toUpload);

  return 0;
}

void * threadReception(void * param)
{
  //cout<<"Thread de réception "<<pthread_self()<<" créé avec succès !"<<endl;
  parameters_t * p = (parameters_t *) param;
  string path, complete_path;
  ofstream file;
  unsigned int size_of_buffer = 4096; //4Ko
  char * buffer = new char[size_of_buffer];
  ssize_t n;
  uint32_t total, f_size;
  
  while (p->go_on)
  {
    pthread_mutex_lock(p->lock);
    while ((p->go_on) && (p->toProcess->size()==0))
    {
      pthread_cond_wait(p->cond,p->lock);
    }
    if (p->toProcess->size()>0)
    {      
      path = p->toProcess->front(); //On récupère le nom de fichier
      p->toProcess->pop_front(); //Et on l'enlève de la liste
      complete_path = FILES_DIRECTORY_C;
      complete_path+=path;
      pthread_mutex_lock(&lock_file_list);
      f_size = file_list[path].size;
      pthread_mutex_unlock(&lock_file_list);
      _V cout<<"Création du fichier "<<path<<" de "<<f_size<<" octets (chemin complet : "<<complete_path<<" )"<<endl;
      file.open(complete_path.c_str(),ios::out | ios::trunc | ios::binary ); //Ouvrir le fichier
      pthread_mutex_unlock(p->lock);
      //Le fichier vide a été créé dans la table et le mutex bloqué
      total=0;
      if (file.is_open())
      {
	//cout<<"Le fichier est correctement ouvert"<<endl;
	while ((total<f_size) && ((n=recv(p->desc,buffer,size_of_buffer,0))>0))
	{
	  file.write(buffer,n);
	  total+=n;
	}
	cout << "Fichier écrit "<<endl;
	file.close();
	pthread_mutex_lock(&lock_file_list);
	pthread_mutex_unlock(&(file_list[path].lock)); //Le fichier étant créé, on le dévérouille
	pthread_mutex_unlock(&lock_file_list);
      }
      else
      {
	_V cerr<<"Impossible d'ouvrir le fichier "<<complete_path<<" en écriture"<<endl;
      }
    }
  }
  delete [] buffer;
  pthread_exit(NULL);
}

void * threadEmission(void * param)
{
  //cout<<"Thread d'émission "<<pthread_self()<<" créé avec succès !"<<endl;
  parameters_t * p = (parameters_t *) param;
  string path,filename;
  ifstream file;
  
  unsigned int size_of_buffer = 4096; //4Ko
  char * buffer = new char[size_of_buffer];

  while (p->go_on) //On veut récupérer la liste des fichiers à uploader
  {
    pthread_mutex_lock(p->lock);
    while ((p->go_on) && (p->toProcess->size()==0))
    {
      pthread_cond_wait(p->cond,p->lock); //On attend le signalement d'une modification
    }
    if (p->toProcess->size()>0)
    {
      filename = p->toProcess->front(); //On récupère le nom de fichier
      path=FILES_DIRECTORY_C;
      path+=filename;
      p->toProcess->pop_front(); //Et on l'enlève de la liste
      file.open(path.c_str(),std::fstream::in); //Ouvrir le fichier
      pthread_mutex_unlock(p->lock);
      if (file.is_open())
      {
	while (not file.eof())
	{
	  file.read(buffer,size_of_buffer);
	  send(p->desc,buffer,file.gcount(),0); //On envoie seulement le nombre de caractères effectivement lus
	}
	file.close();
	pthread_mutex_lock(&lock_file_list);
	file_list[filename].readings-=1; //On ne lit plus le fichier
	pthread_mutex_unlock(&lock_file_list);
      }
      else
      {
	cerr<<"Erreur lors de l'ouverture du fichier"<<endl;
      }
    }
  }
  delete [] buffer;
  pthread_exit(NULL);
}

void * threadClient(void * param)
{
  int * temp = (int *)param;
  int descMain = (*temp);
  delete temp;
  char * action = new char [ACTION_SIZE];

  //Création d'un nouveau CV
  Sock BRAttente(SOCK_STREAM,0,0);
  if (not BRAttente.good())
  {
    cerr<<"Erreur lors de la création de la boîte réseau"<<endl;
    shutdown(descMain,SHUT_RDWR);
    close(descMain);
    pthread_exit(NULL);
  }

  if (listen(BRAttente.getsDesc(),1))
  {
    cerr<<"Erreur lors de la création de la file d'attente"<<endl;
    perror("Détails ");
    shutdown(descMain,SHUT_RDWR);
    close(descMain);
    shutdown(BRAttente.getsDesc(),SHUT_RDWR);
    close(BRAttente.getsDesc());
    pthread_exit(NULL);
  }

  unsigned short port;
  struct sockaddr_in sin;
  socklen_t len = sizeof(sin);
  if (getsockname(BRAttente.getsDesc(), (struct sockaddr *)&sin, &len) == -1) //Récupération des infos socket
    perror("getsockname");
  else
    port = ntohs(sin.sin_port);
 
  if (-1==send(descMain,&port,sizeof(unsigned short),0)) //Envoi du numéro de port au client
  {
    cerr<<"Erreur lors de l'envoi des caractéristiques de la BR"<<endl;
    shutdown(descMain,SHUT_RDWR);
    close(descMain);
    shutdown(BRAttente.getsDesc(),SHUT_RDWR);
    close(BRAttente.getsDesc());
    pthread_exit(NULL);
  }
  
  int descUD;
  struct sockaddr_in BRud;
  socklen_t lgBRud = sizeof(struct sockaddr_in);
  descUD = accept(BRAttente.getsDesc(),(struct sockaddr *)&BRud,&lgBRud); //Création du CV secondaire (upload & download)
  //shutdown(BRAttente.getsDesc(),SHUT_RDWR); //Fermeture de la BR liste d'attente
  //close(BRAttente.getsDesc());
  
  pthread_t tEmission, tReception;
  pthread_cond_t cond_reception= PTHREAD_COND_INITIALIZER;
  list<string>  toDownload;
  pthread_mutex_t lock_toDownload = PTHREAD_MUTEX_INITIALIZER;
  pthread_cond_t cond_emission = PTHREAD_COND_INITIALIZER;
  list<string> toUpload;
  pthread_mutex_t lock_toUpload = PTHREAD_MUTEX_INITIALIZER;
  
  parameters_t parameters_reception;
  parameters_reception.desc = descUD; parameters_reception.cond = &cond_reception; parameters_reception.go_on = true;
  parameters_reception.lock=&lock_toDownload; parameters_reception.toProcess=&toDownload;
  parameters_t parameters_emission;
  parameters_emission.desc = descUD; parameters_emission.cond = &cond_emission; parameters_emission.go_on = true;
  parameters_emission.lock=&lock_toUpload; parameters_emission.toProcess=&toUpload;

  if (pthread_create(&tReception,NULL,threadReception,(void *)&parameters_reception) != 0)
  {
    cerr<<"Erreur lors de la création du thread de réception (thread : "<<pthread_self()<<endl;
    exit(4);
  }
  if (pthread_create(&tEmission,NULL,threadEmission,(void *)&parameters_emission) != 0)
  {
    cerr<<"Erreur lors de la création du thread d'émission (thread : "<<pthread_self()<<endl;
    exit(4);
  }
  sleep(1);
  /************************/
  
  int Rrecv;
  bool go_on = true;
  while (go_on)
  {
    memset(action,0,ACTION_SIZE);
    Rrecv = recv(descMain,action,ACTION_SIZE,0);
    if (Rrecv <=0)
    {
      if (Rrecv == 0) {
	//cerr<<"Client déconnecté"<<endl;
	break;
      }
      else //Rrecv < 0
      {
	cerr<<"Erreur dans la réception"<<endl;
	break;
      }
    }
    switch (getActionType(action))
    {
    case ERROR_NUMBER :
      cerr<<"Erreur : l'action demandée par le client n'a pas de sens \""<<action<<"\""<<endl;
      break;
    case DELETE_NUMBER : //Récuper le nom du fichier, vérifier qu'il existe, préciser qu'on le modifie (lock) en attendant que le
                         //nombre de lectures soit égal à zéro, le supprimer et mettre à jour la table
      if (deleteFile(descMain))
	cerr<<"Erreur lors de la suppression d'un fichier"<<endl;
      break;
    case DOWNLOAD_NUMBER : //Récuper le nom du fichier, vérifier qu'il existe, préciser qu'on le lit en incrémentant le nombre de lecture
                           //et envoyer le fichier
      if (sendFile(descMain,toUpload,lock_toUpload))
	cerr<<"Erreur lors de l'envoi d'un fichier"<<endl;
      else //Tout s'est bien passé, il faut dire au thread de commencer à uploader
      {
	pthread_cond_signal(&cond_emission);
      }
      break;
    case LIST_NUMBER : //Créer le tableau de noms, séparés par "//", envoyer sa taille, puis envoyer le tableau
      if (sendList(descMain))
	cerr<<"Erreur lors de l'envoi de la liste des fichiers"<<endl;
      break;
    case RENAME_NUMBER ://Récuper le nom du fichier, vérifier qu'il existe, préciser qu'on le modifie (lock) en attendant que le
                        //nombre de lectures soit égal à zéro, le renommer et mettre à jour la table
      //if (renameFile(descMain))
	cerr<<"Erreur lors du renommage du fichier"<<endl;
      break;
    case QUIT_NUMBER : //Quitter proprement
      _V cout<<"Le client se déconnecte"<<endl;
      go_on = false;
      break;
    case UPLOAD_NUMBER ://Récupérer le nom de fichier, vérifier qu'il n'existe pas déjà, sinon l'ajouter à la table
                        //et le recevoir
      if (getFile(descMain,&toDownload,lock_toDownload))
	cerr<<"Erreur lors de la réception d'un fichier"<<endl;
      else
      {
	pthread_cond_signal(&cond_reception);
      }
      break;
    default :
      cerr<<"Erreur : type d'action inconnu"<<endl;
      break;
    }  
  }

  //cerr<<"-> Le client de la boîte réseau"<<desc<<" s'est déconnecté"<<endl;
  pthread_mutex_lock(&lock_nb_of_clients);
  nb_of_clients--;
  pthread_mutex_unlock(&lock_nb_of_clients);
  void * b;
  parameters_emission.go_on = false;
  parameters_reception.go_on = false;
  pthread_cond_signal(&cond_reception);
  pthread_cond_signal(&cond_emission);
  pthread_join(tEmission,&b);
  pthread_join(tReception,&b);

  shutdown(descMain,SHUT_RDWR);
  close(descMain);
  shutdown(descUD,SHUT_RDWR);
  close(descUD);
  delete [] action;
  cout<<"-> Client déconnecté ( nombre total : "<<nb_of_clients<<")"<<endl;
  pthread_exit(NULL);
}

int main(int argc, char *argv[], char *envp[])
{
  int queue_len=20;
  if (updateFileList()==-1) //Première initialisation de la liste des fichiers
    return EXIT_FAILURE;
  Sock BRPub(SOCK_STREAM,SERV_PORT,0);
  if (BRPub.good())
  {
    cout<<"# Boîte réseau publique créée : "<<SERV_PORT<<endl;
  }
  else
  {
    cerr<<"Erreur lors de la création de la boîte réseau publique"<<endl;
    exit(1);
  }
  sig_brpub= BRPub.getsDesc();
  signal(SIGINT, sighandler);
  if (listen(BRPub.getsDesc(),queue_len))
  {
    cerr<<"Erreur lors de la création de la file d'attente"<<endl;
    perror("Détails ");
    exit(2);
  }

  cout<<"# Attente de connexions"<<endl;
  
  struct sockaddr_in BRCv;
  socklen_t lgBRCv = sizeof(struct sockaddr_in);

  int descBRCv;
  pthread_t thread_id;
  
  while ((descBRCv=accept(BRPub.getsDesc(),(struct sockaddr *)&BRCv,&lgBRCv)))
  {
    if (descBRCv<=0)
    {
      cerr<<"Erreur lors de l'acceptation d'un nouveau client"<<endl;
      exit(3);
    }
    pthread_mutex_lock(&lock_nb_of_clients);
    cout<<"-> Nouveau client connecté ( nombre total : "<<++nb_of_clients<<")"<<endl;
    pthread_mutex_unlock(&lock_nb_of_clients);

    if (pthread_create( &thread_id , NULL , threadClient, (void*) (new int(descBRCv))))
    {
      cerr<<"Impossible de créer le thread"<<endl;
      exit(4);
    }
    //pthread_join(thread_id, NULL);
    //threadClient(param);
  }
  //shutdown(descBRCv,SHUT_RDWR);
  return EXIT_SUCCESS;
}
